/*
 *   This program is free software: you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation, either version 3 of the License, or
 *   (at your option) any later version.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

/*
 *    Arithmetic.java
 *    Copyright (C) 2008-2012 University of Waikato, Hamilton, New Zealand
 *
 */
package weka.core.pmml;

import java.util.ArrayList;

import weka.core.Attribute;
import weka.core.Utils;

/**
 * Built-in function for min, max, sum, avg, log10, ln, sqrt, abs, exp, pow,
 * threshold, floor, ceil and round.
 * 
 * @author Mark Hall (mhall{[at]}pentaho{[dot]}com)
 * @version $Revision 1.0 $
 */
public class BuiltInMath extends Function {

    /**
     * For serialization
     */
    private static final long serialVersionUID = -8092338695602573652L;

    /**
     * Enum for the math functions.
     */
    enum MathFunc {
        MIN("min") {
            double eval(double[] args) {
                return args[Utils.minIndex(args)];
            }

            boolean legalNumParams(int num) {
                return (num > 0);
            }

            String[] getParameterNames() {
                return null; // unbounded number of parameters
            }
        },
        MAX("max") {
            double eval(double[] args) {
                return args[Utils.maxIndex(args)];
            }

            boolean legalNumParams(int num) {
                return (num > 0);
            }

            String[] getParameterNames() {
                return null; // unbounded number of parameters
            }
        },
        SUM("sum") {
            double eval(double[] args) {
                return Utils.sum(args);
            }

            boolean legalNumParams(int num) {
                return (num > 0);
            }

            String[] getParameterNames() {
                return null; // unbounded number of parameters
            }
        },
        AVG("avg") {
            double eval(double[] args) {
                return Utils.mean(args);
            }

            boolean legalNumParams(int num) {
                return (num > 0);
            }

            String[] getParameterNames() {
                return null; // unbounded number of parameters
            }
        },
        LOG10("log10") {
            double eval(double[] args) {
                return Math.log10(args[0]);
            }

            boolean legalNumParams(int num) {
                return (num == 1);
            }

            String[] getParameterNames() {
                return new String[] { "A" };
            }
        },
        LN("ln") {
            double eval(double[] args) {
                return Math.log(args[0]);
            }

            boolean legalNumParams(int num) {
                return (num == 1);
            }

            String[] getParameterNames() {
                return new String[] { "A" };
            }
        },
        SQRT("sqrt") {
            double eval(double[] args) {
                return Math.sqrt(args[0]);
            }

            boolean legalNumParams(int num) {
                return (num == 1);
            }

            String[] getParameterNames() {
                return new String[] { "A" };
            }
        },
        ABS("abs") {
            double eval(double[] args) {
                return Math.abs(args[0]);
            }

            boolean legalNumParams(int num) {
                return (num == 1);
            }

            String[] getParameterNames() {
                return new String[] { "A" };
            }
        },
        EXP("exp") {
            double eval(double[] args) {
                return Math.exp(args[0]);
            }

            boolean legalNumParams(int num) {
                return (num == 1);
            }

            String[] getParameterNames() {
                return new String[] { "A" };
            }
        },
        POW("pow") {
            double eval(double[] args) {
                return Math.pow(args[0], args[1]);
            }

            boolean legalNumParams(int num) {
                return (num == 2);
            }

            String[] getParameterNames() {
                return new String[] { "A", "B" };
            }
        },
        THRESHOLD("threshold") {
            double eval(double[] args) {
                if (args[0] > args[1]) {
                    return 1.0;
                } else {
                    return 0.0;
                }
            }

            boolean legalNumParams(int num) {
                return (num == 2);
            }

            String[] getParameterNames() {
                return new String[] { "A", "B" };
            }
        },
        FLOOR("floor") {
            double eval(double[] args) {
                return Math.floor(args[0]);
            }

            boolean legalNumParams(int num) {
                return (num == 1);
            }

            String[] getParameterNames() {
                return new String[] { "A" };
            }
        },
        CEIL("ceil") {
            double eval(double[] args) {
                return Math.ceil(args[0]);
            }

            boolean legalNumParams(int num) {
                return (num == 1);
            }

            String[] getParameterNames() {
                return new String[] { "A" };
            }
        },
        ROUND("round") {
            double eval(double[] args) {
                return Math.round(args[0]);
            }

            boolean legalNumParams(int num) {
                return (num == 1);
            }

            String[] getParameterNames() {
                return new String[] { "A" };
            }
        };

        abstract double eval(double[] args);

        abstract boolean legalNumParams(int num);

        abstract String[] getParameterNames();

        private final String m_stringVal;

        MathFunc(String funcName) {
            m_stringVal = funcName;
        }

        public String toString() {
            return m_stringVal;
        }
    }

    /** The function to apply */
    protected MathFunc m_func = MathFunc.ABS;

    /**
     * Construct a new built-in pmml Math function.
     * 
     * @param func the math function to use
     */
    public BuiltInMath(MathFunc func) {
        m_func = func;
        m_functionName = m_func.toString();
    }

    /**
     * Set the structure of the parameters that are expected as input by this
     * function. This must be called before getOutputDef() is called.
     * 
     * @param paramDefs the structure of the input parameters
     * @throws Exception if the number or types of parameters are not acceptable by
     *                   this function
     */
    public void setParameterDefs(ArrayList<Attribute> paramDefs) throws Exception {
        m_parameterDefs = paramDefs;

        if (!m_func.legalNumParams(m_parameterDefs.size())) {
            throw new Exception("[BuiltInMath] illegal number of parameters for function: " + m_functionName);
        }
    }

    /**
     * Get the structure of the result produced by this function. Subclasses must
     * implement.
     * 
     * @return the structure of the result produced by this function.
     */
    public Attribute getOutputDef() {
        return new Attribute("BuiltInMathResult:" + m_func.toString());
    }

    /**
     * Returns an array of the names of the parameters expected as input by this
     * function. May return null if the function can accept an unbounded number of
     * arguments.
     * 
     * @return an array of the parameter names (or null if the function can accept
     *         any number of arguments).
     */
    public String[] getParameterNames() {
        return m_func.getParameterNames();
    }

    /**
     * Get the result of applying this function.
     * 
     * @param incoming the arguments to this function (supplied in order to match
     *                 that of the parameter definitions
     * @return the result of applying this function. When the optype is categorical
     *         or ordinal, an index into the values of the output definition is
     *         returned.
     * @throws Exception if there is a problem computing the result of this function
     */
    public double getResult(double[] incoming) throws Exception {
        if (m_parameterDefs == null) {
            throw new Exception("[BuiltInMath] incoming parameter structure has not been set");
        }

        if (!m_func.legalNumParams(incoming.length)) {
            throw new Exception("[BuiltInMath] wrong number of parameters!");
        }

        double result = m_func.eval(incoming);

        return result;
    }

    public String toString() {
        String result = m_func.toString() + "(";
        for (int i = 0; i < m_parameterDefs.size(); i++) {
            result += m_parameterDefs.get(i).name();
            if (i != m_parameterDefs.size() - 1) {
                result += ", ";
            } else {
                result += ")";
            }
        }
        return result;
    }
}
